﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace ClearingGraphAutomaton
{
    public partial class ClearingGraphForm : Form
    {
        public ClearingGraphForm()
        {
            InitializeComponent();

            this.toolStripComboBoxGraph.SelectedIndex = 0;
            this.toolStripComboBoxNodes.SelectedIndex = 0;
            this.toolStripComboBoxSelectionContext.SelectedIndex = 0;
        }

        private int zoomCounter;
        private readonly int maxZoomCounter = 90;
        private float zoom = 1.0f;
        private readonly float maxZoom = 5.0f;

        private Base.Coordinates coordinates = new Base.Coordinates();
        private Base.Graph graph = null;
        private Base.Graph.Node cursorNode = null;
        private Base.Graph.Selection selection = null;

        private Bitmap image;
        private int[,] imageColors;

        private bool activeSelection = false;
        private Rectangle cursorSelectionRectangle;
        private RectangleF coordinateSelectionRectangle;
        private readonly int minSelectionSize = 10;

        private Point capturedCursor;
        private Point capturedAutoScrollPosition;

        private Rectangle GetDrawingPanelRectangle()
        {
            return new Rectangle(Point.Empty, this.drawingPanel.AutoScrollMinSize);
        }

        private Point GetDrawingPanelCursorPosition(Point cursor)
        {
            Point scrollOffset = this.drawingPanel.AutoScrollPosition;
            Point panelPosition = this.drawingPanel.PointToClient(cursor);
            return new Point(panelPosition.X - scrollOffset.X, panelPosition.Y - scrollOffset.Y);
        }

        private void UpdateZoom(bool updateAutoScrollPosition)
        {
            Point scrollOffset = this.drawingPanel.AutoScrollPosition;
            Point panelPosition = this.GetDrawingPanelCursorPosition(Cursor.Position);
            float oldZoom = this.zoom;
            this.zoom = 1.0f + (this.maxZoom - 1.0f) * (float)this.zoomCounter / (float)this.maxZoomCounter;
            int newDrawingPanelWidth = (int)(this.drawingPanel.ClientSize.Width * zoom);
            int newDrawingPanelHeight = (int)(this.drawingPanel.ClientSize.Height * zoom);
            this.drawingPanel.AutoScrollMinSize = new Size(newDrawingPanelWidth, newDrawingPanelHeight);
            if (updateAutoScrollPosition)
            {
                Point newClientOffsetPosition = new Point(
                    (int)(panelPosition.X * this.zoom / oldZoom),
                    (int)(panelPosition.Y * this.zoom / oldZoom));
                this.drawingPanel.AutoScrollPosition = new Point(
                    newClientOffsetPosition.X - panelPosition.X - scrollOffset.X,
                    newClientOffsetPosition.Y - panelPosition.Y - scrollOffset.Y);
            }
        }

        private void ClearImage()
        {
            if (this.image != null)
            {
                this.image.Dispose();
                this.image = null;
                this.imageColors = null;
            }
        }

        private void ClearSelection()
        {
            this.textBoxSelection.Text = String.Empty;
            this.cursorNode = null;
            this.selection = null;
            this.activeSelection = false;
            this.coordinateSelectionRectangle = new RectangleF(0.0f, 0.0f, 0.0f, 0.0f);
            this.cursorSelectionRectangle = new Rectangle(0, 0, 0, 0);
        }

        private void LoadImage()
        {
            OpenFileDialog openFileDialog = new OpenFileDialog();
            openFileDialog.Filter = "Image Files|*.jpeg;*.jpg;*.gif;*.bmp;*.png|All Files|*.*";
            openFileDialog.Title = "Open Image File";
            if (openFileDialog.ShowDialog() == System.Windows.Forms.DialogResult.OK)
            {
                this.ClearImage();
                try
                {
                    this.image = new Bitmap(openFileDialog.FileName);
                    BitmapData bmpData = this.image.LockBits(new Rectangle(Point.Empty, this.image.Size), 
                        ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
                    // Get the address of the first line.
                    IntPtr ptr = bmpData.Scan0;
                    int stride = bmpData.Stride;
                    // Declare an array to hold the bytes of the bitmap.
                    int bytes = stride * this.image.Size.Height;
                    byte[] rgbValues = new byte[bytes];
                    // Copy the RGB values into the array.
                    System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes);
                    this.imageColors = new int[image.Size.Height, image.Size.Width];
                    for (int row = 0; row < this.image.Size.Height; ++row)
                        for (int col = 0; col < this.image.Size.Width; ++col)
                        {
                            int offset = stride * row + 3 * col;
                            this.imageColors[row, col] = (
                                (int)rgbValues[offset + 0] +
                                (int)rgbValues[offset + 1] +
                                (int)rgbValues[offset + 2]) / 3;
                            // Make the image more transparent.
                            rgbValues[offset + 0] = (byte)(255 - (255 - (int)rgbValues[offset + 0]) / 4);
                            rgbValues[offset + 1] = (byte)(255 - (255 - (int)rgbValues[offset + 1]) / 4);
                            rgbValues[offset + 2] = (byte)(255 - (255 - (int)rgbValues[offset + 2]) / 4);
                        }
                    // Copy the RGB values back to the bitmap
                    System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes);
                    // Unlock the bits.
                    this.image.UnlockBits(bmpData);
                }
                catch (Exception e)
                {
                    this.image = null;
                    MessageBox.Show(e.Message, "Error");
                }
            }
        }

        private int GetCenterColor(RectangleF clipRect)
        {
            if (this.image != null && clipRect.Width > 0 && clipRect.Height > 0)
            {
                int row = (int)(clipRect.Y + clipRect.Height / 2.0f);
                int col = (int)(clipRect.X + clipRect.Width / 2.0f);
                return (row >= 0 && row < this.image.Size.Height &&
                    col >= 0 && col < this.image.Size.Width &&
                    this.imageColors[row, col] < 128) ? 1 : 0;
            }
            else return 0;
        }

        private int GetThresholdColor(RectangleF clipRect, float threshold)
        {
            int totalBlack = 0;
            if (this.image != null && clipRect.Width > 0 && clipRect.Height > 0)
            {
                for (int i = 0; i < 10; ++i)
                    for (int j = 0; j < 10; ++j)
                    {
                        int row = (int)(clipRect.Y + (i + 0.5f) * clipRect.Height / 10.0f);
                        int col = (int)(clipRect.X + (j + 0.5f) * clipRect.Width / 10.0f);
                        if (row >= 0 && row < this.image.Size.Height &&
                           col >= 0 && col < this.image.Size.Width &&
                           this.imageColors[row, col] < 128)
                        {
                            ++totalBlack;
                        }
                    }
            }
            return (totalBlack > threshold * 100.0f) ? 1 : 0;
        }

        private void BuildGraph()
        {
            switch (this.toolStripComboBoxGraph.SelectedIndex)
            {
                case 0: this.graph = null; break;
                case 1: this.graph = new Base.Graph(this.coordinates.Rows + 2, this.coordinates.Columns + 2, Base.Graph.GraphType.Normal); break;
                case 2: this.graph = new Base.Graph(this.coordinates.Rows + 2, this.coordinates.Columns + 2, Base.Graph.GraphType.Diagonal); break;
            }
            if (this.graph != null)
            {
                for (int row = 0; row < this.coordinates.Rows + 2; ++row)
                {
                    this.graph[row, 0].Color = -1;
                    this.graph[row, this.coordinates.Columns + 1].Color = -1;
                }
                for (int col = 0; col < this.coordinates.Columns + 2; ++col)
                {
                    this.graph[0, col].Color = -1;
                    this.graph[this.coordinates.Rows + 1, col].Color = -1;
                }
            }
            if (this.graph != null && this.image != null && this.toolStripComboBoxNodes.SelectedIndex > 0)
            {
                Rectangle clientRectangle = this.drawingPanel.ClientRectangle;
                for (int row = 1; row <= this.coordinates.Rows; ++row)
                    for (int col = 1; col <= this.coordinates.Columns; ++col)
                    {
                        RectangleF clipRectangle = this.coordinates.GetImageClipRectangle(
                            row, col, this.image, clientRectangle);
                        switch (this.toolStripComboBoxNodes.SelectedIndex)
                        {
                            case 1: this.graph[row, col].Color = this.GetCenterColor(clipRectangle); break;
                            case 2: this.graph[row, col].Color = this.GetThresholdColor(clipRectangle, 0.5f); break;
                            case 3: this.graph[row, col].Color = this.GetThresholdColor(clipRectangle, 0.3f); break;
                            case 4: this.graph[row, col].Color = this.GetThresholdColor(clipRectangle, 0.1f); break;
                        }
                    }
            }
        }

        private void BuildSelection()
        {
            if (this.selection != null && this.selection.IsValid)
            {
                StringBuilder stringBuilder = new StringBuilder(1024);
                //stringBuilder.Append("Max Depth = ");
                //stringBuilder.Append(this.selection.MaxContextDepth);
                //stringBuilder.AppendLine();
                //stringBuilder.Append("Max Distance = ");
                //stringBuilder.Append(this.selection.MaxContextDistance);
                //stringBuilder.AppendLine();
                //stringBuilder.Append("Body Size = ");
                //stringBuilder.Append(this.selection.Rows);
                //stringBuilder.Append(" x ");
                //stringBuilder.Append(this.selection.Columns);
                //stringBuilder.AppendLine();
                stringBuilder.Append("Body: ");
                stringBuilder.AppendLine();
                stringBuilder.Append(this.selection.GetSelectionBodyString());
                stringBuilder.AppendLine();
                if (this.toolStripComboBoxSelectionContext.SelectedIndex > 0)
                {
                    stringBuilder.Append("Context: ");
                    stringBuilder.AppendLine();
                }
                switch (this.toolStripComboBoxSelectionContext.SelectedIndex)
                {
                    case 1:
                        stringBuilder.Append(this.selection.GetSelectionBodyString(
                            this.selection.MaxContextDepth));
                        break;
                    case 2:
                        stringBuilder.Append(this.selection.GetSelectionContextString(
                            this.selection.MaxContextDepth, this.selection.MaxContextDistance, false));
                        break;
                    case 3:
                        stringBuilder.Append(this.selection.GetSelectionContextString(
                            this.selection.MaxContextDepth, this.selection.MaxContextDistance, true));
                        break;
                }
                this.textBoxSelection.Text = stringBuilder.ToString();
            }
            else
            {
                this.textBoxSelection.Text = String.Empty;
            }
        }

        private void RebuildAll()
        {
            this.toolStripButtonHand.Checked = true;
            this.toolStripButtonSelection.Checked = false;
            this.ClearSelection();
            this.BuildGraph();
            this.InvalidateAll();
        }

        private void InvalidateAll()
        {
            this.drawingPanel.Invalidate();
        }

        private void InvalidateNode(Base.Graph.Node node, Rectangle rect)
        {
            if (node != null)
            {
                this.InvalidateRectangle(this.coordinates.GetInvalidateRectangle(node.Row, node.Col, rect));
            }
        }

        private void InvalidateRectangle(Rectangle invalidateRectangle)
        {
            Rectangle rect = new Rectangle(
                (int)(Math.Max(0, invalidateRectangle.X + this.drawingPanel.AutoScrollPosition.X)),
                (int)(Math.Max(0, invalidateRectangle.Y + this.drawingPanel.AutoScrollPosition.Y)),
                (int)(invalidateRectangle.Width),
                (int)(invalidateRectangle.Height));
            this.drawingPanel.Invalidate(rect);
        }

        private void InvalidateMargins(Rectangle invalidateRectangle, int d)
        {
            this.InvalidateRectangle(
                new Rectangle(
                    invalidateRectangle.X - d, 
                    invalidateRectangle.Y - d,
                    invalidateRectangle.X + invalidateRectangle.Width + d, 
                    invalidateRectangle.Y + d));
            this.InvalidateRectangle(
                new Rectangle(
                    invalidateRectangle.X - d, 
                    invalidateRectangle.Y + invalidateRectangle.Height - d,
                    invalidateRectangle.X + invalidateRectangle.Width + d, 
                    invalidateRectangle.Y + invalidateRectangle.Height + d));
            this.InvalidateRectangle(
                new Rectangle(
                    invalidateRectangle.X - d, 
                    invalidateRectangle.Y - d,
                    invalidateRectangle.X + d, 
                    invalidateRectangle.Y + invalidateRectangle.Height + d));
            this.InvalidateRectangle(
                new Rectangle(
                    invalidateRectangle.X + invalidateRectangle.Width - d, 
                    invalidateRectangle.Y - d,
                    invalidateRectangle.X + invalidateRectangle.Width + d, 
                    invalidateRectangle.Y + invalidateRectangle.Height + d));
        }

        private static int Distance(Point p1, Point p2)
        {
            return Math.Abs(p1.X - p2.X) + Math.Abs(p1.Y - p2.Y);
        }

        private void drawingPanel_Paint(object sender, PaintEventArgs e)
        {
            Graphics dc = e.Graphics;
            Point scrollOffset = this.drawingPanel.AutoScrollPosition;
            Rectangle clientRectangle = this.drawingPanel.ClientRectangle;
            dc.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
            dc.TranslateTransform(scrollOffset.X, scrollOffset.Y);
            dc.ScaleTransform(this.zoom, this.zoom);
            if (this.image != null)
            {
                coordinates.DrawImage(dc, this.image, clientRectangle);
            }
            coordinates.DrawCoordinates(dc, clientRectangle);
            if (this.graph != null)
            {
                coordinates.DrawGraph(dc, graph, cursorNode, selection, clientRectangle);
            }
            if (this.activeSelection)
            {
                coordinates.DrawSelectionBox(dc, this.coordinateSelectionRectangle, clientRectangle);
            }
        }

        private void drawingPanel_Layout(object sender, LayoutEventArgs e)
        {
            this.UpdateZoom(false);
            this.drawingPanel.Invalidate();
        }

        private void drawingPanel_MouseDown(object sender, MouseEventArgs e)
        {
            if (e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                this.capturedCursor = Cursor.Position;
                this.capturedAutoScrollPosition = this.drawingPanel.AutoScrollPosition;
                if (this.cursorNode != null)
                {
                    this.InvalidateNode(this.cursorNode, this.GetDrawingPanelRectangle());
                    this.cursorNode = null;
                }
                this.activeSelection = true;
                this.cursorSelectionRectangle = new Rectangle(0, 0, 0, 0);
                this.coordinateSelectionRectangle = new RectangleF(0, 0, 0, 0);
            }
            this.drawingPanel.Focus();
        }

        private void drawingPanel_MouseUp(object sender, MouseEventArgs e)
        {
            Point cursor = Cursor.Position;
            if (this.toolStripButtonSelection.Checked && e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                this.activeSelection = false;
                Rectangle drawingPanelRectangle = this.GetDrawingPanelRectangle();
                Point drawingPanelCursorPosition = this.GetDrawingPanelCursorPosition(cursor);
                Point capturedDrawingPanelCursorPosition = this.GetDrawingPanelCursorPosition(this.capturedCursor);
                if (Distance(drawingPanelCursorPosition, capturedDrawingPanelCursorPosition) < this.minSelectionSize)
                {
                    Base.Graph.Node node = this.coordinates.GetNodeAtCursor(
                        this.graph, drawingPanelCursorPosition, drawingPanelRectangle);
                    if (node != null)
                    {
                        this.selection = new Base.Graph.Selection(this.graph, node.Row, node.Col,
                            (int)this.toolStripNumericUpDownContextDepth.Value,
                            (int)this.toolStripNumericUpDownContextDistance.Value);
                        this.InvalidateAll();
                    }
                    else if (this.selection != null)
                    {
                        this.selection = null;
                        this.InvalidateAll();
                    }
                }
                else
                {
                    this.selection = this.coordinates.GetSelectionInArea(
                        this.graph, this.cursorSelectionRectangle, 
                        (int)this.toolStripNumericUpDownContextDepth.Value,
                        (int)this.toolStripNumericUpDownContextDistance.Value,
                        drawingPanelRectangle);
                    this.InvalidateAll();
                }
                this.BuildSelection();
            }
        }

        private void drawingPanel_MouseMove(object sender, MouseEventArgs e)
        {
            Point cursor = Cursor.Position;
            if (this.toolStripButtonHand.Checked && e.Button == System.Windows.Forms.MouseButtons.Left)
            {
                int deltaX = cursor.X - this.capturedCursor.X;
                int deltaY = cursor.Y - this.capturedCursor.Y;
                this.drawingPanel.AutoScrollPosition = new Point(
                    -this.capturedAutoScrollPosition.X - deltaX, 
                    -this.capturedAutoScrollPosition.Y - deltaY);
            }
            else
            {
                Rectangle drawingPanelRectangle = this.GetDrawingPanelRectangle();
                Point drawingPanelCursorPosition = this.GetDrawingPanelCursorPosition(cursor);
                Point capturedDrawingPanelCursorPosition = this.GetDrawingPanelCursorPosition(this.capturedCursor);
                if (this.toolStripButtonSelection.Checked)
                {
                    if (e.Button == System.Windows.Forms.MouseButtons.Left)
                    {
                        int selectionX = Math.Min(drawingPanelCursorPosition.X, capturedDrawingPanelCursorPosition.X);
                        int selectionY = Math.Min(drawingPanelCursorPosition.Y, capturedDrawingPanelCursorPosition.Y);
                        int selectionWidth = Math.Abs(drawingPanelCursorPosition.X - capturedDrawingPanelCursorPosition.X);
                        int selectionHeight = Math.Abs(drawingPanelCursorPosition.Y - capturedDrawingPanelCursorPosition.Y);
                        this.InvalidateMargins(cursorSelectionRectangle, (int)(4 * this.zoom));
                        this.cursorSelectionRectangle = new Rectangle(selectionX, selectionY, selectionWidth, selectionHeight);
                        this.coordinateSelectionRectangle = this.coordinates.GetCoordinateRectangle(cursorSelectionRectangle, drawingPanelRectangle);
                        this.InvalidateMargins(cursorSelectionRectangle, (int)(4 * this.zoom));
                    }
                    else
                    {
                        Base.Graph.Node node = this.coordinates.GetNodeAtCursor(
                            this.graph, drawingPanelCursorPosition, drawingPanelRectangle);
                        if (node != null && node.Color == -1)
                        {
                            node = null;
                        }
                        if (this.cursorNode != node)
                        {
                            this.InvalidateNode(this.cursorNode, drawingPanelRectangle);
                            this.cursorNode = node;
                            this.InvalidateNode(this.cursorNode, drawingPanelRectangle);
                        }
                    }
                }
                PointF cursorCoordinates = this.coordinates.GetCoordinates(
                    drawingPanelCursorPosition, drawingPanelRectangle);
                this.toolStripStatusCoordinatesLabel.Text =
                    String.Format("Coordinates: {0:F}; {1:F}", cursorCoordinates.X, cursorCoordinates.Y);
            }
        }

        private void drawingPanel_MouseWheel(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            if (this.toolStripButtonHand.Checked)
            {
                int numberOfTextLinesToMove = e.Delta * SystemInformation.MouseWheelScrollLines / 120;
                this.zoomCounter += numberOfTextLinesToMove;
                this.zoomCounter = Math.Max(0, this.zoomCounter);
                this.zoomCounter = Math.Min(this.maxZoomCounter, this.zoomCounter);
                this.UpdateZoom(true);
            }
        }

        private void clearToolStripMenuItem_Click(object sender, EventArgs e)
        {
            this.ClearImage();
            this.RebuildAll();
        }

        private void loadToolStripMenuItem_Click(object sender, EventArgs e)
        {
            this.LoadImage();
            this.RebuildAll();
        }

        private void toolStripNumericUpDownRows_ValueChanged(object sender, EventArgs e)
        {
            this.coordinates.Rows = (int)this.toolStripNumericUpDownRows.Value;
            this.RebuildAll();
        }

        private void toolStripNumericUpDownColumns_ValueChanged(object sender, EventArgs e)
        {
            this.coordinates.Columns = (int)this.toolStripNumericUpDownColumns.Value;
            this.RebuildAll();
        }

        private void toolStripComboBoxGraph_SelectedIndexChanged(object sender, EventArgs e)
        {
            this.RebuildAll();
        }

        private void toolStripComboBoxNodes_SelectedIndexChanged(object sender, EventArgs e)
        {
            this.RebuildAll();
        }

        private void toolStripButtonHand_Click(object sender, EventArgs e)
        {
            this.toolStripButtonHand.Checked = true;
            this.toolStripButtonSelection.Checked = false;
        }

        private void toolStripButtonSelection_Click(object sender, EventArgs e)
        {
            this.toolStripButtonHand.Checked = false;
            this.toolStripButtonSelection.Checked = true;
        }

        private void toolStripNumericUpDownContextDepth_ValueChanged(object sender, EventArgs e)
        {
            if (this.selection != null)
            {
                if (this.selection.ResizeContext(
                    (int)this.toolStripNumericUpDownContextDepth.Value,
                    (int)this.toolStripNumericUpDownContextDistance.Value))
                {
                    this.InvalidateAll();
                }
            }
            this.BuildSelection();
        }

        private void toolStripNumericUpDownContextDistance_ValueChanged(object sender, EventArgs e)
        {
            if (this.selection != null)
            {
                if (this.selection.ResizeContext(
                    (int)this.toolStripNumericUpDownContextDepth.Value,
                    (int)this.toolStripNumericUpDownContextDistance.Value))
                {
                    this.InvalidateAll();
                }
            }
            this.BuildSelection();
        }

        private void toolStripComboBoxSelection_SelectedIndexChanged(object sender, EventArgs e)
        {
            this.BuildSelection();
        }

        private void toolStripButtonClear_Click(object sender, EventArgs e)
        {
            if (this.selection != null && this.selection.IsValid)
            {
                this.selection.ClearSelection();
                this.ClearSelection();
                this.InvalidateAll();
            }
        }
    }
}
